/**
 * com.chaeron.GPS - An API for access to GPS devices.
 *
 *
 * Copyright (c) 2001, 2002 Chaeron Corporation,
 * All rights reserved.
 * 
 * Chaeron grants you a non-exclusive license to use, modify and re-distribute
 * this program under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version, provided that this copyright notice and 
 * license appear on all copies of the software.
 * 
 * Software is provided "AS IS," without a warranty of any kind. ALL EXPRESS OR
 * IMPLIED REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE
 * HEREBY EXCLUDED. THE ENTIRE RISK ARISING OUT OF USING THE SOFTWARE IS ASSUMED
 * BY THE LICENSEE. See the GNU General Public License for more details.
 *
 * CHAERON AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY
 * LICENSEE OR ANY THIRD PARTY AS A RESULT OF USING OR DISTRIBUTING SOFTWARE.
 * IN NO EVENT WILL CHAERON OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE,
 * PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL
 * OR PUNITIVE DAMAGES, HOWEVER CAUSED AN REGARDLESS OF THE THEORY OF LIABILITY,
 * ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF CHAERON HAS
 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

 * @version     1.02
 * @author      Andrzej Jan Taramina, Chaeron Corporation
 *              andrzej@chaeron.com
 */

package com.chaeron.GPS.NMEA;

//***************************************************************************
//*
//*     Imports
//*
//***************************************************************************/

import com.chaeron.GPS.*;
import com.chaeron.GPS.event.*;
import com.chaeron.GPS.event.decorator.*;
import com.chaeron.GPS.exception.*;

import com.chaeron.platform.common.*;

//***************************************************************************
//*
//*     NMEASentence Class definition
//*
//***************************************************************************/

/**
 * The NMEASentence class is used to communicate NMEA Sentences
 *
 */

public class NMEASentence implements GPSExtraFields, Parseable, GPSValue
{
	
	//***************************************************************************
	//*
	//*     Constant definitions
	//*
	//***************************************************************************/
	
	// Talker ID's
	
	protected final static int		NMEA_TALKER_ERROR					= -1;
	protected final static int		NMEA_TALKER_UNKNOWN					= 0;
	protected final static int		NMEA_TALKER_PROPRIETARY				= 1;
	protected final static int		NMEA_TALKER_GPS						= 2;
	protected final static int		NMEA_TALKER_MAGNETIC_COMPASS		= 3;
	
	protected final static char	 	NMEA_TALKER_VALUE_PROPRIETARY		= 'P';
	
	private final static String[]	talkerIDArray						=
	{
		"GP",
		"HC"
	};
	
	protected final static String 	NMEA_TALKER_VALUE_GPS				= "GP";
	
	
	// Sentence ID's
	
	protected final static int		NMEA_SENTENCE_UNKNOWN				= 0;
	protected final static int		NMEA_SENTENCE_BOD					= 1;
	protected final static int		NMEA_SENTENCE_GGA					= 2;
	protected final static int		NMEA_SENTENCE_GLL					= 3;
	protected final static int		NMEA_SENTENCE_GSA					= 4;
	protected final static int		NMEA_SENTENCE_GSV					= 5;
	protected final static int		NMEA_SENTENCE_HDG					= 6;
	protected final static int		NMEA_SENTENCE_RMB					= 7;
	protected final static int		NMEA_SENTENCE_RMC					= 8;
	protected final static int		NMEA_SENTENCE_RTE					= 9;
	
	private final static String[]	sentenceIDArray						=
	{
		"BOD",
		"GGA",
		"GLL",
		"GSA",
		"GSV",
		"HDG",
		"RMB",
		"RMC",
		"RTE",
	};
	
	
	// Manufacturer ID's (start at 10000 since these overlap with sentence ID's)
	
	protected final static int		NMEA_MANUFACTURER_UNKNOWN			= 10000;
	protected final static int		NMEA_MANUFACTURER_GARMIN			= 10001;
	
	private final static String[]	manufacturerIDArray					=
	{
		"GRM"
	};
	
	private final static char		NMEA_GARMIN_POSITIONERROR			= 'E';
	private final static char		NMEA_GARMIN_ALTITUDE				= 'Z';
	private final static char		NMEA_GARMIN_MAPDATUM				= 'M';
	
	
	//***************************************************************************
	//*
	//*     Attributes
	//*
	//***************************************************************************/
	
	private int						talkerID							= NMEA_TALKER_UNKNOWN;
	private int						sentenceID							= NMEA_SENTENCE_UNKNOWN;
	private String					sentenceString						= null;
	
	private int						fieldCount							= 0;
	private Vector					fields								= new Vector( 20 );
	
	private byte					checksum							= 0;
	
	private boolean					parsed								= false;	
	
	private int						methodsImplemented					= 0;
	
	//***************************************************************************
	//*
	//*     Constructors/Factory Methods
	//*
	//***************************************************************************/
	
	/**
	 * Factory method to create a subclass of NMEASentence instance from a given packet byte array
	 *
	 * @param	packet		packet data buffer
	 * @param	len			length of packet data
	 * @param	gps			current gps processor instance
	 * @throws GPSPacketException
	 */
	
	static NMEASentence createInstance( GPS gps, byte[] packet, int len ) throws GPSPacketException
	{  
		NMEASentence		sentence		= null;
		char[]				sb 				= new char[ len ];
		String				sentenceStr;
		int					i;
		int					parseError;
		int					tid;
		int					sid;
		
		for( i = 0; i < len; i++ ) {
			sb[ i ] = (char)packet[ i ];
		}
		
		sentenceStr = new String( sb );
		
		tid = getTalkerID( sentenceStr );
		sid = getSentenceID( tid, sentenceStr );
		
		switch( tid ) {
			case NMEA_TALKER_GPS:
				sentence = createGPSSentence( gps, tid, sid, sentenceStr );
				break;
				
			case NMEA_TALKER_MAGNETIC_COMPASS:
				sentence = createMagCompassSentence( gps, tid, sid, sentenceStr );
				break;
				
			case NMEA_TALKER_PROPRIETARY:
				sentence = createProprietarySentence( gps, tid, sid, sentenceStr );
				break;
				
			case NMEA_TALKER_ERROR:
				throw( new GPSPacketException( GPSPacketException.EXCEPTION_PACKETID_INVALID ) );
				
			default:
				if( gps.isEventListened( GPSEvent.GPS_EVENT_NMEA_SENTENCE ) ) {
					sentence = new NMEASentence( tid, sid, sentenceStr );		// Unknown so create a generic sentence
				}
				break;
		}
		
		return( sentence );
	}
	
	
	/**
	 * Create a NMEASentence instance from a given String Buffer
	 *
	 * @param	tid			talker id
	 * @param	sid			sentence id
	 * @param	sb			sentence string buffer
	 */
	
	protected NMEASentence( int tid, int sid, String sentenceStr )
	{  
		talkerID 		= tid;
		sentenceID 		= sid;
		sentenceString 	= sentenceStr;
	}
	
	
	/**
	 * Create GPS Sentence
	 *
	 * @param	tid			talker id
	 * @param	sid			sentence id
	 * @param	sentenceStr	sentence string
	 * @param	gps			current gps processor instance
	 * @return				created GPS Sentence object
	 */
	
	private static NMEASentence createGPSSentence( GPS gps, int tid, int sid, String sentenceStr )
	{
		NMEASentence	sentence		= null;
		
		switch( sid ) {
			case NMEA_SENTENCE_GGA:
				if( Compile.INCLUDE_NMEA_SENTENCE_GGA ) {
					if( gps.isEventListened( GGASentence.getEventsGenerated() ) ) {
						sentence = new GGASentence( tid, sid, sentenceStr );
					}
				} 
				break;
				
			case NMEA_SENTENCE_BOD:
				if( Compile.INCLUDE_NMEA_SENTENCE_BOD ) {
					if( gps.isEventListened( BODSentence.getEventsGenerated() ) ) {
						sentence = new BODSentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_SENTENCE_GSA:
				if( Compile.INCLUDE_NMEA_SENTENCE_GSA ) {
					if( gps.isEventListened( GSASentence.getEventsGenerated() ) ) {
						sentence = new GSASentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_SENTENCE_GSV:
				if( Compile.INCLUDE_NMEA_SENTENCE_GSV ) {
					if( gps.isEventListened( GSVSentence.getEventsGenerated() ) ) {
						sentence = new GSVSentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_SENTENCE_RMC:
				if( Compile.INCLUDE_NMEA_SENTENCE_RMC ) {
					if( gps.isEventListened( RMCSentence.getEventsGenerated() ) ) {
						sentence = new RMCSentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_SENTENCE_RMB:
				if( Compile.INCLUDE_NMEA_SENTENCE_RMB ) {
					if( gps.isEventListened( RMBSentence.getEventsGenerated() ) ) {
						sentence = new RMBSentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_SENTENCE_GLL:
				if( Compile.INCLUDE_NMEA_SENTENCE_GLL ) {
					if( gps.isEventListened( GLLSentence.getEventsGenerated() ) ) {
						sentence = new GLLSentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_SENTENCE_RTE:
				if( Compile.INCLUDE_NMEA_SENTENCE_RTE ) {
					if( gps.isEventListened( RTESentence.getEventsGenerated() ) ) {
						sentence = new RTESentence( tid, sid, sentenceStr );
					}
				}
				break;
				
		}
		
		if( sentence == null && gps.isEventListened( GPSEvent.GPS_EVENT_NMEA_SENTENCE ) ) {
			sentence = new NMEASentence( tid, sid, sentenceStr );				// Unknown so create a generic sentence
		}
		
		return( sentence );
	}
	
	
	/**
	 * Create Magnetic Compass Sentence
	 *
	 * @param	tid			talker id
	 * @param	sid			sentence id
	 * @param	sentenceStr	sentence string
	 * @param	gps			current gps processor instance
	 * @return				created Magnetic Compass Sentence object
	 */
	
	private static NMEASentence createMagCompassSentence( GPS gps, int tid, int sid, String sentenceStr )
	{
		NMEASentence	sentence		= null;
		
		switch( sid ) {
			case NMEA_SENTENCE_HDG:
				if( Compile.INCLUDE_NMEA_SENTENCE_HDG ) {
					if( gps.isEventListened( HDGSentence.getEventsGenerated() ) ) {
						sentence = new HDGSentence( tid, sid, sentenceStr );
					}
				}
				break;
		}
		
		if( sentence == null && gps.isEventListened( GPSEvent.GPS_EVENT_NMEA_SENTENCE ) ) {
			sentence = new NMEASentence( tid, sid, sentenceStr );				// Unknown so create a generic sentence
		}
		
		return( sentence );
	}
	
	
	/**
	 * Create Proprietary Sentence
	 *
	 * @param	tid			talker id
	 * @param	sid			sentence id
	 * @param	sentenceStr	sentence string
	 * @param	gps			current gps processor instance
	 * @return				created Proprietary Sentence object
	 */
	
	private static NMEASentence createProprietarySentence( GPS gps, int tid, int sid, String sentenceStr )
	{
		NMEASentence	sentence		= null;
		char			pid				= 0;
		
		pid = sentenceStr.charAt( 5 );
		
		switch( sid ) {
			case NMEA_MANUFACTURER_GARMIN:
				sentence = createGarminSentence( gps, tid, sid, pid, sentenceStr );
				break;
		}
		
		if( sentence == null && gps.isEventListened( GPSEvent.GPS_EVENT_NMEA_SENTENCE ) ) {
			sentence = new NMEASentence( tid, sid, sentenceStr );				// Unknown so create a generic sentence
		}
		
		return( sentence );
	}
	
	
	/**
	 * Create Garmin Proprietary Sentence
	 *
	 * @param	tid			talker id
	 * @param	sid			sentence id
	 * @param	sentenceStr	sentence string
	 * @param	gps			current gps processor instance
	 * @return				created Garmin roprietary Sentence object
	 */
	
	private static NMEASentence createGarminSentence( GPS gps, int tid, int sid, char pid, String sentenceStr )
	{
		NMEASentence	sentence		= null;
		
		switch( pid ) {
			case NMEA_GARMIN_POSITIONERROR:
				if( Compile.INCLUDE_NMEA_SENTENCE_PGRME ) {
					if( gps.isEventListened( PGRMESentence.getEventsGenerated() ) ) {
						sentence = new PGRMESentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_GARMIN_ALTITUDE:
				if( Compile.INCLUDE_NMEA_SENTENCE_PGRMZ ) {
					if( gps.isEventListened( PGRMZSentence.getEventsGenerated() ) ) {
						sentence = new PGRMZSentence( tid, sid, sentenceStr );
					}
				}
				break;
				
			case NMEA_GARMIN_MAPDATUM:
				if( Compile.INCLUDE_NMEA_SENTENCE_PGRMM ) {
					if( gps.isEventListened( PGRMMSentence.getEventsGenerated() ) ) {
						sentence = new PGRMMSentence( tid, sid, sentenceStr );
					}
				}
				break;
				
		}
		
		if( sentence == null && gps.isEventListened( GPSEvent.GPS_EVENT_NMEA_SENTENCE ) ) {
			sentence = new NMEASentence( tid, sid, sentenceStr );				// Unknown so create a generic sentence
		}
		
		return( sentence );
	}
	
	
	//***************************************************************************
	//*
	//*     Sentence Parsing Methods
	//*
	//***************************************************************************/
	
	/**
	 * Determine the talker ID
	 *
	 * @param	sb			sentence string buffer
	 * @return				talker ID
	 */
	
	protected static int getTalkerID( String sentenceStr )
	{  
		int 		talker 	= 	NMEA_TALKER_UNKNOWN;
		int			len;
		int			arrayLen;
		int			i;
		String		tid;
		
		len 		= sentenceStr.length();
		
		if( len < 9 ) {
			talker = 	NMEA_TALKER_ERROR;
		} else {
			if( sentenceStr.charAt( 0 ) != NMEAGPSImpl.BEGIN_PACKET
				|| sentenceStr.charAt( len - 1 ) != NMEAGPSImpl.END_PACKET2
				|| sentenceStr.charAt( len - 2 ) != NMEAGPSImpl.END_PACKET1 ) {
				talker = 	NMEA_TALKER_ERROR;
			} else {
				tid = sentenceStr.substring( 1, 3 );
				if( tid.charAt( 0 ) == NMEA_TALKER_VALUE_PROPRIETARY ) {
					talker = NMEA_TALKER_PROPRIETARY;
				} else {
					arrayLen = talkerIDArray.length;
					
					for( i = 0; i < arrayLen; i++ ) {
						if( tid.equals( talkerIDArray[ i ] ) ) {
							talker = i + NMEA_TALKER_GPS;
							break;
						}
					}
				}
			}
		}
		
		return( talker );
	}
	
	
	/**
	 * Determine the sentence ID.  This will be either the sentence ID for normal sentences
	 * or the Manufacturer ID for Proprietary sentences.
	 *
	 * @param	sb			sentence string buffer
	 * @return				sentence ID
	 */
	
	protected static int getSentenceID( int tid, String sentenceStr )
	{  
		int id = 	NMEA_SENTENCE_UNKNOWN;
		String		sid		= "";
		int			i;
		int			offset;
		int			len;
		String[]	idArray;
		
		if( tid != NMEA_TALKER_ERROR ) {
			len 		= sentenceStr.length();
			
			if( tid == NMEA_TALKER_PROPRIETARY ) {
				sid 	= sentenceStr.substring( 2, 5 );
				offset 	= NMEA_MANUFACTURER_UNKNOWN + 1;
				idArray = manufacturerIDArray;
			} else {
				sid 	= sentenceStr.substring( 3, 6 );
				offset 	= NMEA_SENTENCE_UNKNOWN + 1;
				idArray = sentenceIDArray;
			}
			
			len = idArray.length;
			
			for( i = 0; i < len; i++ ) {
				if( sid.equals( idArray[ i ] ) ) {
					id = i + offset;
					break;
				}
			}
		}
		
		return( id );
	}
	
	
	/**
	 * Parse the fields out
	 */
	
	protected void	parseFields()
	{
		char[]				field;
		int					fieldEnd 	= 7;
		int					fieldStart 	= 7;
		int					i;
		int					len;
		char				chr;
		
		len = sentenceString.length() - 2;
		
		field = new char[ len ];
		
		for( i = 7; i < len; i++ ) {
			chr = sentenceString.charAt( i );
			if( chr == NMEAGPSImpl.FIELD_SEPARATOR ) {
				if( fieldEnd > fieldStart ) {
					fields.add( sentenceString.substring( fieldStart, fieldEnd  ) );
				} else {
					fields.add( "" );
				}
				fieldEnd   = i + 1;
				fieldStart = i + 1;
			} else if( chr == NMEAGPSImpl.BEGIN_CHECKSUM ) {
				if( fieldEnd > fieldStart ) {
					fields.add( sentenceString.substring( fieldStart, fieldEnd  ) );
				} else {
					fields.add( "" );
				}
				fieldEnd   = i + 1;
				fieldStart = i + 1;
				parseChecksum( i + 1 );
				break;
			} else {
				fieldEnd++;
			}
		}
	}
	
	
	/**
	 * Decode the checksum value in the sentence string
	 *
	 * @param	i	checksum starting index
	 */
	
	protected void parseChecksum( int i )
	{
		if( i == sentenceString.length() - 4 ) {
			checksum = (byte)( ( hexCharToByte( sentenceString.charAt( i ) ) & 0xF ) << 4 );
			checksum = (byte)( checksum | hexCharToByte( sentenceString.charAt( i + 1 ) ) & 0xF );
		} else {
			checksum = (byte)( calcChecksum() + 1 );   //  Set an invalid checksum!
		}
	}
	
	
	/**
	 * Get the byte value for a hex character
	 *
	 * @param	hexChar		hex character
	 * @return				byte value
	 */
	
	private byte hexCharToByte( char hexChar )
	{
		byte		value	= 0;
		
		if( hexChar > 57 ) {
			value = (byte)( hexChar - 55 );
		} else {
			value = (byte)( hexChar - 48 ); 
		}
		
		return( (byte)value );
		
	}
	
	
	//***************************************************************************
	//*
	//*     Sentence Processing Methods (overriden by subclasses)
	//*
	//***************************************************************************/
	
	/**
	 * Process the sentence
	 *
	 * @param	gps			the GPS object that requested the sentence to be processed
	 */
	
	void process( GPS gps )
	{
		gps.postGPSEvent( new GPSEvent( GPSEvent.GPS_EVENT_UNKNOWN_DATA, this ) );
	}
	
	
	//***************************************************************************
	//*
	//*     Accessor Methods
	//*
	//***************************************************************************/
	
	/**
	 * Get sentence String
	 *
	 * @return		sentence 
	 */
	
	public String getSentence()
	{
		return( sentenceString );   
	}
	
	
	/**
	 * Get Sentence Length
	 *
	 * @return		sentence length
	 */
	
	int getSentenceLength()
	{
		return( sentenceString.length() );   
	}
	
	
	/**
	 * Get number of fields
	 *
	 * @return		# of fields
	 */
	
	int getFieldCount()
	{
		return( fields.getCount() );   
	}
	
	
	/**
	 * Get field value for specified index
	 *
	 * @return		field value
	 */
	
	String getField( int index )
	{
		return( (String)fields.get( index ) );   
	}
	
	
	/**
	 * Get the checksum 
	 *
	 * @return		checksum
	 */
	
	public byte getChecksum()
	{
		return( checksum );   
	}
	
	
	/**
	 * Validate the checksum 
	 *
	 * @return		is valid?
	 */
	
	public boolean validateChecksum()
	{
		return( getChecksum() == calcChecksum() );   
	}
	
	
	
	/**
	 * Calculate the checksum from the data value
	 *
	 * @return		the calculated checksum
	 */
	
	public byte calcChecksum()
	{
		return( calcChecksum( sentenceString ) );
	}
	
	/**
	 * Calculate the checksum from the data value
	 * @param		sentence	sentence to calculate checksum for
	 * @return		the calculated checksum
	 */
	
	public static byte calcChecksum( String sentence )
	{
		int		calc = 0;
		int		i;
		int		len;
		char	chr;
		
		len = sentence.length();
		
		for( i = 1; i < len - 2; i++ ) {
			chr = sentence.charAt( i );
			
			if( chr != NMEAGPSImpl.BEGIN_CHECKSUM ) {
				if( i == 1 ) {
					calc = ( chr + 256 ) & 0xFF;
				} else {
					calc ^= ( chr + 256 ) & 0xFF;
				}
			} else {
				break;
			}
		}
		
		return( (byte)calc );
	}
	
	
	/**
	 * Calculate the checksum from the data value and return as a string
	 * @param		sentence	sentence to calculate checksum for
	 * @return		the calculated checksum as a string
	 */
	
	public static String calcChecksumAsString( String sentence )
	{
		byte		calc = 0;
		String		csum = "";
		byte		chr1;
		byte		chr2;
		
		calc = calcChecksum( sentence );
	
		chr1 = (byte)( ( calc & 0xF0 ) >>> 4 );
		chr2 = (byte)( calc & 0x0F );
		
		if( chr1 < 0x0A ) {
			csum += (char)('0' + chr1);
		} else {
			csum += (char)('A' + chr1 - 10);
		}
		
		if( chr2 < 0x0A ) {
			csum += (char)('0' + chr2);
		} else {
			csum += (char)('A' + chr2 - 10);
		}
		
		return( csum );
	}
	
	
	/**
	 * Parse the data fields.
	 *
	 * @return		This method should return a standard error code or GPS.ERROR_OK
	 * @throws GPSPacketException
	 * @throws GPSFunctionUnavailableException
	 */
	
	public void parse() throws GPSPacketException, GPSFunctionUnavailableException
	{  
		if( !parsed ) {
			parseFields();
			if( validateChecksum() ) {
				parseData();
			} else {
				throw( new GPSPacketException( GPSPacketException.EXCEPTION_INVALID_CHECKSUM ) );
			}
			
			parsed = true;
		}
	}
	
	
	/**
	 * Parse the data fields. Overriden by subclasses
	 *
	 * @return		This method should return a standard error code or GPS.ERROR_OK
	 * @throws GPSPacketException
	 * @throws GPSFunctionUnavailableException
	 */
	
	protected void parseData() throws GPSPacketException, GPSFunctionUnavailableException
	{  
		throw( new GPSFunctionUnavailableException() );
	}
	
	
	/**
	 * Does the object contain valid data?
	 *
	 * @return		Does the object contain valid data?
	 */
	
	public boolean isDataValid()
	{  
		return( true );
	}
	
	
	/**
	 * Were the data fields parsed already? 
	 *
	 * @return		true if parsed already
	 */
	
	public boolean isParsed()
	{  
		return( parsed );
	}
	
	
	/**
	 * Set the expected field count for this sentence
	 *
	 * @return		expected field count for this sentence
	 */
	
	protected void setFieldCount( int count )
	{  
		fieldCount = count;
	}
	
	
	/**
	 * Get extra field count
	 *
	 * @return		number of extra fields
	 */
	
	public int getExtraFieldCount()
	{  
		int		count	= 0;
		
		if( fields.getCount() > fieldCount ) {
			count = fields.getCount() - fieldCount;
		}
		
		return( count );
	}
	
	
	/**
	 * Get extra field value for the specified index
	 *
	 * @param		index of field to be retrieved
	 * @return		extra field value (null if index out of range)
	 */
	
	public String getExtraField( int index )
	{  
		String		value = null;
		int			count;
		
		count = getExtraFieldCount();
		
		if( index >= 0 && index < count ) {
			value = (String)fields.get( fieldCount + index );	
		}
		
		return( value );
	}
	
	
	/**
	 * Get events generated. Overriden by subclasses
	 *
	 * @return		return a valid event type(s) that will be generated by the sentence subclass
	 */
	
	static int getEventsGenerated()
	{  
		return( 0 );
	}
	
	
	/**
	 * Return a string representation of the sentence
	 *
	 * @return		string representation of the sentence
	 */
	
	public String toString()
	{  
		String		str		= sentenceString;
		int			pos;
		
		pos = str.length() - 1;
		
		if( str.charAt( pos ) == 10 ) {
			str = str.substring( 0, pos - 1 );
		}
		
		return( str );
	}
	
	
	//***************************************************************************
	//*
	//*     Packet Data Conversion Methods
	//*
	//***************************************************************************/
	
	/**
	 * Get an NMEA Int field
	 *
	 * @param	str		string value
	 * @return			integer value
	 */
	
	protected int getNMEAInt( String str )
	{  
		int	value = 0;
		
		if( str.length() > 0 ) {
			try {
				value = Convert.parseInt( str );
			} 
			catch( Exception e ) {
			}
		}

		return( value );
	}
	
	
	/**
	 * Get an NMEA short field
	 *
	 * @param	str		string value
	 * @return			short value
	 */
	
	protected short getNMEAShort( String str )
	{  
		return( (short)getNMEAInt( str ) );
	}
	
	
	/**
	 * Get an NMEA char field
	 *
	 * @param	str		string value
	 * @return			char value
	 */
	
	protected char getNMEAChar( String str )
	{  
		char		value	= 0;
		
		if( str.length() > 0 ) {
			value = str.charAt( 0 );
		}
		
		return( value );
	}
	
	
	/**
	 * Get an NMEA Float field
	 *
	 * @param	str		string value
	 * @return			float value
	 */
	
	protected float getNMEAFloat( String str )
	{  
		float		value;
		float		fraction	= 0.0f;
		float		integral	= 0.0f;
		int			divisor		= 1;
		int			i;
		int			decimal;
		int			len;
		
		len 	= str.length();
		decimal	= len;
		
		for( i = 0; i < len; i++ ) {
			if( str.charAt( i ) == '.' ) {
				decimal = i;
				break;
			}
		}
		
		if( decimal > 0 ) {
			integral = getNMEAInt( str.substring( 0, decimal ) );
		}
		
		if( decimal < len - 1 ) {
			fraction = getNMEAInt( str.substring( decimal + 1, len ) );
			for( i = 0; i < len - decimal - 1; i++ ) {
				divisor = divisor * 10;
			}
			fraction = fraction / divisor;
		}
		
		if( len > 0 && str.charAt( 0 ) == '-' ) {
			value = integral - fraction;
		} else {
			value = integral + fraction;
		}
		
		return( value );
	}
	
	/**
	 * Get an NMEA Latitude or Longitude field
	 *
	 * @param	str			string value
	 * @param	direction	direction (N/S/E/W)
	 * @return				float value
	 */
	
	protected float getNMEALatLong( String str, String direction )
	{  
		float		value	= 0;
		float		temp;
		int			degrees;
		
		temp = getNMEAFloat( str );
		
		degrees = (int)temp / 100;
		
		value = ( ( temp - (float)( degrees * 100 ) ) / 60.0f ) + (float)degrees;
		
		if( direction.equals( "S" ) || direction.equals( "W" ) ) {
			value = -value;
		}
		
		return( value );
	}
	
	
	/**
	 * Get an NMEA Degrees field
	 *
	 * @param	str			string value
	 * @param	direction	direction (N/S/E/W)
	 * @return				float value
	 */
	
	protected float getNMEADegrees( String str, String direction )
	{  
		float		value	= 0;
		
		value = getNMEAFloat( str );
		
		if( direction.equals( "S" ) || direction.equals( "W" ) ) {
			value = -value;
		}
		
		return( value );
	}
	
	
	//***************************************************************************
	//*
	//*    Dummy (do nothing) GPSValue Methods
	//*
	//***************************************************************************/
	
	/**
	 * Get the Methods Implemented flags
	 *
	 * @return		methods implemented flags
	 */
	
	public int getMethodsImplemented() 
	{
		return( methodsImplemented );
	}
	
	/**
	 * Set the methodsImplemented flags
	 *
	 * @param	flags	field flags
	 */
	
	public void setMethodsImplemented( int flags ) 
	{
		methodsImplemented = flags;
	}
	
	/**
	 * Test if a method is implemented
	 *
	 * @param	methodFlag	method to test
	 */
	
	public boolean isMethodImplemented( int methodFlag )
	{
		return( ( methodsImplemented & methodFlag ) != 0 );
	}
	
	/**
	 * Return this Event Value as XML
	 *
	 * @return Event Value as XML
	 */
	
	public String toXML() 
	{
		String		tag  = "";
		
		if( Compile.INCLUDE_EVENT_TOXML ) {
			tag = GPSEventDecorator.genXMLValueTag( GPSXMLTag.XML_TAG_NMEASENTENCE, toString() );
		}
		
		return( tag );
	}
	
	/**
	 * Return this Event Value as XML
	 *
	 * @param declaration include XML Declaration at start?
	 * @param namespace include XML namespace at start?
	 * @return Event Value as XML
	 */
	
	public String toXML( boolean declaration, boolean namespace )
	{
		StringBuffer 	tag		= new StringBuffer();
		
		if( Compile.INCLUDE_EVENT_TOXML ) {
			if( declaration ) {
				tag.append( GPSXMLTag.XML_DECLARATION );
			}
			
			tag.append( GPSEventDecorator.genXMLTag( GPSXMLTag.XML_TAG_NMEASENTENCE, null, toString(), true, true, namespace ) );
		}
		
		return( tag.toString() );
	}
	
	/**
	 * Write this Event Value as XML to a catalog
	 *
	 * @param catalog	catalog to write to.
	 */
	
	public void toXML( Catalog catalog ) 
	{
		if( Compile.INCLUDE_EVENT_TOXML && Compile.INCLUDE_EVENT_SAVETOCATALOG  ) {
			String	tag;
			
			tag = GPSEventDecorator.genXMLValueTag( GPSXMLTag.XML_TAG_NMEASENTENCE, toString() );
			GPSEventDecorator.writeString( catalog, tag );
		}
	}
	
	/**
	 * Write this Event Value as XML to a catalog
	 *
	 * @param catalog	catalog to write to.
	 * @param declaration include XML Declaration at start?
	 * @param namespace include XML namespace at start?
	 */
	
	public void toXML( Catalog catalog, boolean declaration, boolean namespace ) 
	{
		if( Compile.INCLUDE_EVENT_TOXML && Compile.INCLUDE_EVENT_SAVETOCATALOG  ) {
			String	tag;
			
			if( declaration ) {
				tag = GPSXMLTag.XML_DECLARATION;
				GPSEventDecorator.writeString( catalog, tag );
			}
			
			tag = GPSEventDecorator.genXMLTag( GPSXMLTag.XML_TAG_NMEASENTENCE, null, toString(), true, true, namespace );
			GPSEventDecorator.writeString( catalog, tag );
		}
	}
	
}
